#include "torrentconnection.h"
#include "torrentpeerdata.h"
#include "trackerrequest.h"
#include "torrentpeerobserver.h"
#include "constants.h"
#include <QTimerEvent>
#include <QTimer>

TorrentConnection::TorrentConnection(TorrentPeerData data, TorrentPeerObserver& observer,
                                     QObject *parent) :
        QObject(parent), peerData(data), peerObserver(observer),
        connectionState(NotConnected), dataAmountToReceive(0)

{    
    timerKeepAliveSending = new QTimer(this);
    timerKeepAliveReceiving = new QTimer(this);
    timerPieceDownloading = new QTimer(this);
    timerKeepAliveSending->setSingleShot(false);
    timerKeepAliveSending->setInterval(KEEP_ALIVE_SENDING_INTERVAL_MSEC);
    timerKeepAliveReceiving->setInterval(KEEP_ALIVE_TIMEOUT_MSEC);
    this->socket = new QTcpSocket(this);
    readBuffer = new QByteArray();
    pieceDownloaded = new QByteArray();
}
TorrentConnection::~TorrentConnection()
{
    delete readBuffer;
    delete pieceDownloaded;
}

void TorrentConnection::init(const QByteArray &aInfoHash)
{
    //start connection, send handshake
    this->infoHash = aInfoHash;
    disconnect(socket, SIGNAL(connected()), this, SLOT(connectedToPeer()));
    bool result = connect(socket, SIGNAL(connected()), this, SLOT(connectedToPeer()));
    bool result1 = connect(socket, SIGNAL(connected()), timerKeepAliveReceiving, SLOT(start()));
    bool result2 = connect(socket, SIGNAL(readyRead()), this, SLOT(dataReceived()));
    bool result3 = connect(socket, SIGNAL(readyRead()), timerKeepAliveReceiving, SLOT(start()));
    bool result4 = connect(timerKeepAliveReceiving, SIGNAL(timeout()),
                           this, SLOT(timedOut()));
    bool result5 = connect(socket, SIGNAL(disconnected()), this, SLOT(peerDisconnected()));
    bool result6 = connect(timerKeepAliveSending, SIGNAL(timeout()), this, SLOT(sendKeepAlive()));
    //ensure that signal-slots connections are correct
    Q_ASSERT(result && result1 && result2 && result3 && result4 && result5 && result6);
    socket->connectToHost(peerData.ipAddress, peerData.port);
}
void TorrentConnection::stop()
{
    socket->disconnectFromHost();
    timerKeepAliveReceiving->stop();
    timerKeepAliveSending->stop();
}

void TorrentConnection::connectedToPeer()
{
    QString message = "connected to peer: " + socket->peerAddress().toString();
    qDebug() << message;
    connectionState= WaitingForHandshake;
    sendHandshake();
}

void TorrentConnection::peerDisconnected()
{
    QString message = "peer disconnected: " + socket->peerAddress().toString();
    qDebug() << message;
    connectionState = NotConnected;
    timerKeepAliveReceiving->stop();
    timerKeepAliveSending->stop();
    if(!peerData.peerChoking)
    {
        peerObserver.chokeReceived(*this);
    }
}

void TorrentConnection::sendHandshake()
{
    //handshake formed as described in:
    //http://wiki.theory.org/index.php/BitTorrentSpecification#Handshake

    Q_ASSERT(PROTOCOL_ID_LENGTH < 255);
    char protocolIdLength = PROTOCOL_ID_LENGTH;    
    QByteArray protocolId(PROTOCOL_ID, protocolIdLength);
    QByteArray peerId = QString(TrackerRequest::getPeerId()).toAscii();
    QByteArray reserved(8, '\0');
    Q_ASSERT(peerId.length() < 255);
    int handshakeSize = 1 + protocolId.size() + infoHash.size() + reserved.size() + peerId.size();

    QString debugMsg = "sending handshake to: " + socket->peerAddress().toString();
    qDebug() << debugMsg << ", handshake messagesize: " << QString::number(handshakeSize);
    //TODO send as single socket write
    QByteArray bytesToSend;
    bytesToSend.append(protocolIdLength + protocolId + reserved + infoHash + peerId );
//   checkError( socket->write(&protocolIdLength, 1));
//    checkError(socket->write(PROTOCOL_ID, protocolIdLength));
//    checkError(socket->write(reserved.data(), 8));
//    checkError( socket->write(infoHash.data(), infoHash.length()) );
//   checkError( socket->write(peerId.data(), (qint8) peerId.length()) );
    socket->write(bytesToSend);

}
void TorrentConnection::setPeerInteresting(bool isInteresting)
{
    qDebug() << "TorrentConnection:setPeerInteresting(" << isInteresting << ")";
    peerData.amInterested = isInteresting;
    char msgId = (char) (isInteresting ? Interested : NotInterested);
    quint32 interestingMsgLenght = 1;
    QByteArray arrayToSend = convertFromLittleEndian(interestingMsgLenght);
    arrayToSend.append(msgId);
    socket->write(arrayToSend);
}

void TorrentConnection::sendKeepAlive()
{
    qDebug() << "TorrentConnection: sendKeepAlive: sending to "
            << peerData.ipAddress.toString() ;
    QByteArray keepAliveMsg(4, '\0');   //fortunately zeroes are endianness-independent :)
    socket->write(keepAliveMsg);
    if (!timerKeepAliveSending->isActive())
        timerKeepAliveSending->start();

}
void TorrentConnection::sendRequestPiece(int pieceIndex, int begin, int length)
{
    qDebug() << "TorrentConnection: sendRequestPiece( " << pieceIndex << ", "
            << begin << ", " << length << ")";
    const quint32 requestMessageLength = 13;
    char requestMessageId = (char) TorrentConnection::Request;
    QByteArray bytesToSend;    
    bytesToSend.append( convertFromLittleEndian(requestMessageLength) );
    bytesToSend.append(requestMessageId);
    bytesToSend.append(convertFromLittleEndian(pieceIndex) + convertFromLittleEndian(begin)
                  + convertFromLittleEndian(length) );
    Q_ASSERT(bytesToSend.size() == requestMessageLength + sizeof(requestMessageLength));
    QByteArray bytesPrintable;
    for(int i=0; i < bytesToSend.size(); i++ )
    {
        bytesPrintable.append(QByteArray::number(bytesToSend.at(i)));        
    }
    qDebug() << "sending bytes: " << bytesPrintable;
    socket->write(bytesToSend);
}

void TorrentConnection::cancelPiece(int pieceIndex, int begin, int length)
{
    qDebug() << "TorrentConnection: sendCancelPiece( " << pieceIndex << ", "
            << begin << ", " << length << ")";
    qDebug() << " (peer " + peerData.ipAddress.toString() + ")";
    //clear buffer etc
    readBuffer->clear();
    socket->readAll();
    dataAmountToReceive = 0;
    //send CANCEL message
    const quint32 cancelMessageLength = 13;
    char cancelrequestMessageId = (char) TorrentConnection::Cancel;
    QByteArray bytesToSend;
    bytesToSend.append( convertFromLittleEndian(cancelMessageLength) );
    bytesToSend.append(cancelrequestMessageId);
    bytesToSend.append(convertFromLittleEndian(pieceIndex) + convertFromLittleEndian(begin)
                  + convertFromLittleEndian(length) );
    Q_ASSERT(bytesToSend.size() == cancelMessageLength + sizeof(cancelMessageLength));
    socket->write(bytesToSend);

}

void TorrentConnection::dataReceived()
{
    //TODO code becomes horrible - think about refactoring it before forgetting how it works ...
    qDebug() << "";
    qint64 dataSize = socket->bytesAvailable();
    QString debugMsg = "received data from peer:  " + socket->peerAddress().toString()
                       + ", bytes: " + QString::number(dataSize);
    qDebug() << debugMsg;
    if(WaitingForHandshake == connectionState)
    {
        QByteArray handshakeData = socket->read(HANDSHAKE_SIZE);
        receiveHandshake(handshakeData);
        dataSize = socket->bytesAvailable();
    }

    if(dataAmountToReceive)
    {
        //we are waiting until buffer size reaches expected payload size
        qDebug() << "received " << dataSize << "bytes, expected: "
                << dataAmountToReceive << " bytes";
        if(dataSize >= dataAmountToReceive)
        {

            *readBuffer += socket->read(dataAmountToReceive);
            dispatchMessage(*readBuffer);
            readBuffer->clear();
            socket->readAll();
            dataAmountToReceive = 0;
        }
    }
    else
    {
        while(dataSize)
        {
            if(dataSize < 4)
            {
                qDebug() << "Invalid message - too short 'size' field'";
                break;
            }
            qDebug() << "bytes left in read buffer: " << dataSize;
            QByteArray msgSizeBytes = socket->read(4);

            quint32 msgSizeField = convertFromBigEndian(msgSizeBytes);
            if( (dataSize - 4) < msgSizeField)
            {
                qDebug() <<"'size' field: " << msgSizeField <<"greater than data size";
                QByteArray msgTypeByte = socket->read(1);
                if(msgTypeByte.isEmpty())
                {
                    qDebug() << "no more data after 'size' field, rejecting";
                    break;
                }
                char messageType = msgTypeByte.at(0);
                if(messageType >= TorrentConnection::Piece
                   && messageType <= TorrentConnection::Cancel)
                {
                    //wait for rest of the data
                    *readBuffer = msgSizeBytes + msgTypeByte;
                    dataAmountToReceive = msgSizeField - 1;
                    qDebug() << "waiting for data of size: "
                            << dataAmountToReceive;
                }
                qDebug() << "message type: " << QString::number(messageType);

                break;
            }
            msgSizeBytes.append(socket->read(msgSizeField));
            dispatchMessage(msgSizeBytes);
            dataSize = socket->bytesAvailable();
            dataAmountToReceive = 0;
        }
    }
    qDebug() << "";
    qDebug() << "---------------------------------------";
    qDebug() << "";

}
void TorrentConnection::dispatchMessage(QByteArray &dataRead)
{

    //message format according to BT protocol specification:
    // <length prefix><message ID><payload>
    //The length prefix is a four byte big-endian value
    //The message ID is a single decimal byte.

    //'convert' message length to little endian
    //TODO there may be problem on Symbian with endianness (on ARM) ?
    qDebug() << "TorrentConnection:dispatchMessage: message length bytes: "
            << QString::number(dataRead.at(0)) << QString::number(dataRead.at(1))
            << QString::number(dataRead.at(2)) << QString::number(dataRead.at(3));

    qint32 messageLength = convertFromBigEndian(dataRead);
    qDebug() << "TorrentConnection:dispatchMessage: from peer " << socket->peerAddress().toString();

        qDebug() << "TorrentConnection:dispatchMessage: messageLength: "
                <<QString::number(messageLength);

        if(0==messageLength)
        {
            qDebug() << "TorrentConnection:dispatchMessage: keep-alive received ";
            return;
        }
        else
        {
            char messageId = dataRead.at(4);
            qDebug() << "TorrentConnection:dispatchMessage: messageId: "
                    <<QString::number(messageId);
            switch(messageId)
            {
            case Choke:
                receiveChoke();
                break;
            case Unchoke:
                receiveUnchoke();
                break;
            case Interested:
                receiveInterested();
                break;
            case NotInterested:
                receiveNotInterested();
                break;
            case Have:
                receiveHave(dataRead);
                break;
            case Bitfield:
                receiveBitfield(dataRead);
                break;
            case Request:
                receiveRequest(dataRead);
                break;
            case Piece:
                receivePiece(dataRead);
                break;
            case Cancel:
                receiveCancel(dataRead);
                break;
            default:
                qDebug() << "TorrentConnection:dispatchMessage: unrecognized message id";
                break;
            }
        }
}
void TorrentConnection::timedOut()
{
    qDebug() << "TorrentConnection:timedOut: connection to "
            << socket->peerAddress().toString() << " timed out";
    socket->disconnectFromHost();
}

void TorrentConnection::receiveHandshake(QByteArray &dataReceived)
{
    qint8 pstrlen = (qint8) dataReceived.at(0);
    QByteArray pstr = dataReceived.mid(1, pstrlen); //protocol Id string
    if (PROTOCOL_ID_LENGTH != pstrlen ||
        QByteArray(PROTOCOL_ID) != pstr)
    {
        qDebug() << "TorrentConnection:receiveHandshake: invalid data";
        return;
    }
    int afterReservedBytesPos = 1 + pstrlen + 8;
    QByteArray infoHashReceived = dataReceived.mid(afterReservedBytesPos, 20);
    QByteArray peerIdReceived = dataReceived.mid(afterReservedBytesPos + 20, 20);
    //TODO disconnect if peerIdReceived or infoHashReceived values
    //differ from appropriate values stored in this->peerData
    qDebug() << "Handshake Received: " << "pstrlen: " << QString::number(pstrlen);
    qDebug() << "pstr: " << QString(pstr);
    qDebug() << "infoHash received: " << QString(infoHashReceived);
    qDebug() << "peerIdReceived: " << QString(peerIdReceived);
    qDebug() << "============================================";
    connectionState = HandshakeReceived;
    //after handshake, sending keep-alive messages should begin
    sendKeepAlive();
}
void TorrentConnection::receiveChoke()
{
    qDebug() << "TorrentConnection:receiveChoke: ";
    peerData.peerChoking = true;
    peerObserver.chokeReceived(*this);
}
void TorrentConnection::receiveUnchoke()
{    
    qDebug() << "TorrentConnection:receiveUnchoke: ";
    peerData.peerChoking = false;
    peerObserver.unchokeReceived(*this);
}
void TorrentConnection::receiveInterested()
{
    qDebug() << "TorrentConnection:receiveInterested: ";
    peerData.peerInterested = true;
}
void TorrentConnection::receiveNotInterested()
{
    qDebug() << "TorrentConnection:receiveNotInterested: ";
    peerData.peerInterested = false;
}
void TorrentConnection::receiveHave(const QByteArray& data)
{
    QByteArray payload = data.mid(5,4);
    quint32 pieceIndex = convertFromBigEndian(payload);
    qDebug() << "TorrentConnection:receiveHave: HAVE piece " << QString::number(pieceIndex);
    if ( (quint32) peerData.piecesOwned.size() <= pieceIndex)
    {
        //invalid piece index - ignore
        return;
    }
    peerData.piecesOwned.setBit(pieceIndex, true);
    //TODO check if this piece is already downloaded, if not mark peer as interested
    if(!peerData.amInterested)
        setPeerInteresting(true);
}
void TorrentConnection::receiveBitfield(const QByteArray& data)
{
    QByteArray lengthField = data.left(4);
    quint32 bitfieldLength = convertFromBigEndian(lengthField) - 1;
    QByteArray bitfieldBytes = data.mid(4);
    if (bitfieldLength > (quint32) peerData.piecesOwned.size()
        || bitfieldLength > (quint32) bitfieldBytes.size())
    {
        qDebug() <<"TorrentConnection:receiveBitfield: invalid bitfield size";
        return;
    }
    QBitArray bitfield(peerData.piecesOwned.size(), false);

    for (int i = 0; i < bitfield.size(); i++)
    {
        int byteIndex = (int) i / 8;
        char currentByte = bitfieldBytes.at(byteIndex);
        bool bitVal = (bool) ( currentByte & (1 << ( i % 8) ) );
        bitfield.setBit(i, bitVal);
    }
    QString toDebug;
    for (int i=0; i <bitfield.size(); i++)
    {
        toDebug.append(QString::number((char) bitfield.at(i)));
    }
    qDebug() << "TorrentConnection:receiveBitfield: received"
            << toDebug;
    peerData.piecesOwned = bitfield;
    //TODO temporary, until some decision mechanism is introduced
    setPeerInteresting(true);

}
void TorrentConnection::receiveRequest(const QByteArray& /*&data*/)
{
    qDebug() << "TorrentConnection:receiveRequest: ";
}
void TorrentConnection::receivePiece(const QByteArray& data)
{
    qDebug() << "TorrentConnection:receivePiece: ";
    peerObserver.pieceReceived(*this, data.mid(5));
}
void TorrentConnection::receiveCancel(const QByteArray& /*&data*/)
{
    qDebug() << "TorrentConnection:receiveCancel: ";
}
quint32 TorrentConnection::convertFromBigEndian(QByteArray &data)
{
    quint32 convertedValue;
    Q_ASSERT(data.size() >=4);
    const unsigned char* dataUchar = (const unsigned char*) data.data();
        convertedValue = (quint32) dataUchar[0] << 24
                        | (quint32) dataUchar[1] << 16
                        | (quint32) dataUchar[2] << 8
                        | (quint32) dataUchar[3];
    return convertedValue;
}
QByteArray TorrentConnection::convertFromLittleEndian(quint32 littleEndianInt)
{
    QByteArray bytesInNetworkOrder(4, '\0');
    bytesInNetworkOrder[3] = littleEndianInt & 0xff;
    bytesInNetworkOrder[2] = (littleEndianInt & 0xff00) >> 8;
    bytesInNetworkOrder[1] = (littleEndianInt & 0xff0000) >> 16;
    bytesInNetworkOrder[0] = (littleEndianInt & 0xff000000) >> 24;
    return bytesInNetworkOrder;
}

void TorrentConnection::checkError(int errorCode)
{
    //equivalent of Symbian User::LeaveIfError :))
    if (-1 == errorCode)
    {
        qDebug() << "An error ocurred";
        //TODO throw exception or sth?

    }
}
bool TorrentConnection::hasPiece(int pieceIndex)
{
    return peerData.hasPiece(pieceIndex);
}
const TorrentPeerData& TorrentConnection::getPeerData()
{
    return peerData;
}
